package auction;

import java.io.IOException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;

import analytics.AnalyticsServer;
import billing.BillingServerSecure;
import event.AuctionEvent;
import event.BidEvent;
import event.UserEvent;

/**
 * Manages User Logins and Auctions
 * 
 * @author Gernot
 * 
 */
public class AccountManager {
    private ArrayList<ClientHandler> connections = new ArrayList<ClientHandler>();
    private HashMap<String, ClientHandler> users = new HashMap<String, ClientHandler>();
    private HashMap<Integer, Auction> auctions = new HashMap<Integer, Auction>();
    private HashMap<Integer, Auction> endedAuctions = new HashMap<Integer, Auction>();
    private ArrayList<Bid> tentative_bids = new ArrayList<Bid>();
    //private HashMap<Integer, Timer> timers = new HashMap<Integer, Timer>();
    private Timer timer = new Timer();
    static int auctionid = 1;
    public static AccountManager instance;
    private AnalyticsServer analyticsServer;
    private BillingServerSecure billingServer;

    /**
     * saves all unauthenticated connections
     * @param ch
     */
    public synchronized void addConnection(ClientHandler ch){
        connections.add(ch);
    }
    
    /**
     * some kind of pseudo-singleton
     * @param analyticsServer The interface of the RMI-object
     */
    public AccountManager(AnalyticsServer analyticsServer, BillingServerSecure billingServer) {
        instance = this;
        this.analyticsServer = analyticsServer;
        this.billingServer = billingServer;
    }

    /**
     * Assigns new Auction-ID
     * 
     * @return
     */
    private Integer getAuctionID() {
        return new Integer(auctionid++);
    }

    /**
     * Creates new Auction
     * @param name
     * @param owner
     * @param seconds
     * @return return-msg
     */
    public synchronized String createAuction(String name, String owner,
            int seconds) {
        /*if (auctions.containsKey(name)) {
            return "Auction creation failed! Name already exists!\n";
        } else */{
            Integer id = getAuctionID();
            Auction a = new Auction(id, name, owner, seconds);
            auctions.put(id, a);
            System.out.println("Created Auction "+id+" "+name+" expires: "+a.getExpires());
            createEndTimer(id,a.getExpires());
            reportAuctionEvent("AUCTION_STARTED", id);
            return "An auction '"+name+"' with id "+id+" has been created and will end on "+a.getExpires()+".";
        }
    }
    
    /**
     * @param user
     * @param id
     * @param amount
     * @return Message
     */
    public synchronized String bidAuction(String user, int id, double amount){
        if (auctions.containsKey(id)){
            return auctions.get(id).bid(new Bid(user, amount, System.currentTimeMillis()));
        } else {
            return "No auction with ID:"+id+" active!";
        } 
    }

    
    /**
     * Starts a Groupbid
     * @param user
     * @param id
     * @param amount
     * @return
     */
    public synchronized String initGroupBid(String user, int id, double amount){
        
        if (auctions.containsKey(id)){
            Bid gbid = new Bid(user, amount, id);
            String err = checkGroupBid(gbid);
            if (err==null){ 
                tentative_bids.add(gbid);
                GroupBidTimeout timeout = new GroupBidTimeout(gbid, this);
                Date d = new Date();
                d.setTime(new Date().getTime()+20000);
                timer.schedule(timeout, d);               
                gbid.setTimeout(timeout);
                
                return "GroupBid started, waiting for confirmation!";                
            } else {
                return err;
            }
             
        } else {
            return "No auction with ID:"+id+" active!\n";
        }
    
    }
    
    private void notifyConfirmers(Bid b, String msg){
        
        String user1 = b.getConfirm1();
        String user2 = b.getConfirm2();
        
        if (user1!=null){
            ClientHandler ch = users.get(user1);
            if (ch!=null){
                try {
                    ch.sendTCPMessage(msg);
                } catch (IOException e) {
                   System.out.println(e.getMessage());
                }
            }
        }
        if (user2!=null){
            ClientHandler ch = users.get(user2);
            if (ch!=null){
                try {
                    ch.sendTCPMessage(msg);
                } catch (IOException e) {
                   System.out.println(e.getMessage());
                }
            }
        }
        
    }
    
    public synchronized String confirmGroupBid(String user, int id, double amount, String inituser){
        Bid gbid = null;
        for (Bid b: tentative_bids){
            if (b.isThisGroupBid(inituser, amount, id)){
                gbid = b;
                break;
            }
        }
        if (gbid!=null){
            if (!gbid.getUser().equals(user)){
                
                if (gbid.confirm(user)){
                    
                    //Bid confirmed!
                    this.tentative_bids.remove(gbid);
                    
                    
                    gbid.getTimeout().cancel(); //Cancel Timeout
                    
                    //try to place bid
                    if (auctions.containsKey(id)){                 
                        
                        
                        String resp = auctions.get(id).bid(gbid.setTimestamp(System.currentTimeMillis()));
                        if (resp.startsWith("You successfully bid")) {
                            notifyConfirmers(gbid, "!confirmed");
                            System.out.println("Group Bid placed");
                            return null ; //"Confirm OK";
                        } else {
                            notifyConfirmers(gbid, "!rejected "+resp);
                            return null; //"Bid denied";
                        }
                    } else {
                        notifyConfirmers(gbid,"!rejected No auction with ID:"+id+" active!\n");
                        return null; //"No auction with ID:"+id+" active!\n";
                    } 
                    
                    
                } else {
                    System.out.println("Waiting for second confirm!");
                    return null;
                }
                
            } else {
                return "You can't confirm your own Bid!";
            }
            
        } else {
            return "No Tentative Bid with ID:"+id+" from "+inituser+" with amount:"+amount+"\n";
        }
        
    }
    
    public synchronized void timeoutGroupbid(Bid bid){
        if (tentative_bids.contains(bid)){
            System.out.println("GroupBid timed out: "+bid);
            this.tentative_bids.remove(bid);
            notifyConfirmers(bid, "!rejected Timeout");
        } else {
            System.out.println("GroupBid no longer active, timeout failed!");
        }
    }
    
    
    private String checkGroupBid(Bid bid){
        if (getUserCount()>tentative_bids.size()){
            for (Bid b: tentative_bids){
              /* if (b.isSameAuctionUser(bid)){
                  return "Same User already started GroupBid on this auction!";
               }*/
                //MAX 1 Tentative Bid per user!
                if (b.isFromSameUser(bid)){
                    return "Max 1 pending Groupbid per User allowed!";
                }
            }
            return null;
        } else {
            return "Number of tentative bids would exceed number of users!";
        }
    }
    
    /**
     * Send udp notification to user
     * @param user
     * @param msg
     */
    public synchronized void notifyUser(String user, String msg) {
        //System.out.println("Contains: "+users.containsKey(user));
        ClientHandler ch = users.get(user);
        if (ch != null) {
            try {
                ch.sendUDPMessage(msg);
            } catch (IOException e) {
                System.out.println("Couldn't send Message: " + e.getMessage());
            }
        } else {
            System.out.println("Client '" + user + "' invalid. logoff");
            this.logout(user);
        }
    }

    /**
     * Creates Timertask that calls endAuction(<id>) on Time Date;
     * 
     * @param id
     *            -id to End
     * @param Date
     *            -When to end auction
     */
    public void createEndTimer(int id, Date date) {
        TimerTask task = new AuctionEndTimer(id);
        timer.schedule(task, date);
    }

    /**
     * ends auction with id, should only be called by AuctionEndTimer;
     * @param id
     */
    public synchronized void endAuction(int id) {
        Auction a = auctions.remove(id);
        
        if (a != null) {
        	reportAuctionEvent("AUCTION_ENDED", id);
        }
        
        //Send Notifications

        String msg = a.getAuctionEndString();
        
        endedAuctions.put(id, a);
        
       // try {
            //users.get(a.getUser()).sendUDPMessage(msg); //can Cause nullpointerException!
            try {
            billingServer.billAuction(a.getUser(), id, a.getBid());
            } catch (RemoteException re){
                System.out.println("ERROR:"+re.getMessage());
            }
        /*} catch (IOException e) {
            //logoff user
            System.out.println("IOException, with user"+a.getUser()+":"+e.getMessage());
            this.logout(a.getUser());
        }*/
        
        if (!a.getBiduser().equals("")){
        	reportBidEvent("BID_WON", a.getBiduser(), id, a.getBid());
            try {
                String u = a.getBiduser();
                if (users.containsKey(u)){
                    users.get(u).sendUDPMessage(msg);
                }
            } catch (IOException e) {
                //logoff user
                System.out.println("IOException, with user"+a.getBiduser()+":"+e.getMessage());
                this.logout(a.getBiduser());
            }
        }
        
    }
    
    /**
     * used for shutdown, stops all timers
     */
    public void stopAllTimers(){
        timer.cancel();
        timer.purge();
    }
    /**
     * used for shutdown, stops all client Threads
     */
    public synchronized void stopClientThreads(){
        System.out.println("Stopping Threads of logged in users...");
        for (String usr : users.keySet()) {
            System.out.println("---Stopping:"+ users.get(usr).toString());
            users.get(usr).stopClientHandler();
        }
        System.out.println("Stopping Threads of connections ("+connections.size()+")...");
        for (int i=0; i<connections.size(); i++){
            System.out.println("---Stopping:"+  connections.get(i).toString());
            connections.get(i).stopClientHandler();
        }
    }
    
    private int getUserCount(){
        return users.size();
    }    
    
    /**
     * Returns All Auctions as Strings
     * @return
     */
    public synchronized ArrayList<String> listAuctions() {
        ArrayList<String> list = new ArrayList<String>();

        for (Integer id : auctions.keySet()) {
            list.add(auctions.get(id).toString());
        }

        if (list.size() == 0) {
            list.add("No auctions active!");
        }

        return list;
    }

    /**
     * Returns the number of active Group-bids as Int 
     * @return
     */
    public synchronized int countGroupBids(){
        int count = 0;

        for (Integer id : auctions.keySet()) {
            Bid bid = auctions.get(id).getHighestBid();
            if (bid !=null){
                if (bid.isGroupBid()){
                    ++count;
                }
            }
        }
        return count;
    }
    
    /**
     * @param usrname
     * @return login successful
     */
    public synchronized boolean login(String usrname, ClientHandler ch) {
        if (!users.containsKey(usrname)) {
            users.put(usrname, ch);
            connections.remove(ch);
            reportUserEvent("USER_LOGIN", usrname);
            //System.out.println("User "+usrname+" logged in.");
            return true;
        }
        return false;
    }

    /**
     * Checks if user login is valid
     * @param user
     * @param ch
     * @return
     */
    public synchronized boolean isLoginValid(String user, ClientHandler ch){
        if (users.containsKey(user)){
            return users.get(user)==ch;
        }
        return false;
    }
    
    /**
     * @param usrname
     * @param clientHandler
     * @return logout successful
     */
    public synchronized boolean logout(String usrname) {
        if (users.containsKey(usrname)) {
            ClientHandler ch = users.remove(usrname);
            connections.add(ch);
            System.out.println("User "+usrname+" logged out.");
            reportUserEvent("USER_LOGOUT", usrname);
            return true;
        }
        return false;
    }
    
    /**
     * Removes Thread from active connection list
     * @param ch
     */
    public synchronized void removeConnection(ClientHandler ch){
        connections.remove(ch);
    }
    
    /**
     * @param usrname
     * @param clientHandler
     * @return logout successful
     */
    public synchronized boolean logout_timeout(String usrname) {
        if (users.containsKey(usrname)) {
            users.remove(usrname);
            System.out.println("User "+usrname+" timed out.");
            reportUserEvent("USER_DISCONNECTED", usrname);
            return true;
        }
        return false;
    }
    
    /**
     * Returns if a ClientHandler Thread is considered as "logged in"
     * @param ch
     * @return
     */
    public synchronized boolean isLoggedIn(ClientHandler ch){
        return users.containsValue(ch);
    }
    
    private void reportAuctionEvent(String type, long auctionID) {
    	AuctionEvent event = new AuctionEvent();
    	event.setId(new java.rmi.server.UID().toString());
    	event.setType(type);
    	event.setTimestamp(System.currentTimeMillis());
    	event.setAuctionID(auctionID);
    	try {
			analyticsServer.processEvent(event);
		} catch (RemoteException e) {
			System.err.println("Could not report event " + type);
		}
    }
    
    void reportUserEvent(String type, String userName) {
    	UserEvent event = new UserEvent();
    	event.setId(new java.rmi.server.UID().toString());
    	event.setType(type);
    	event.setTimestamp(System.currentTimeMillis());
    	event.setUserName(userName);
    	try {
			analyticsServer.processEvent(event);
		} catch (RemoteException e) {
			System.err.println("Could not report event " + type);
		}
    }
    
    void reportBidEvent(String type, String userName, long auctionID, double price) {
    	BidEvent event = new BidEvent();
    	event.setId(new java.rmi.server.UID().toString());
    	event.setType(type);
    	event.setTimestamp(System.currentTimeMillis());
    	event.setUserName(userName);
    	event.setAuctionID(auctionID);
    	event.setPrice(price);
    	try {
			analyticsServer.processEvent(event);
		} catch (RemoteException e) {
			System.err.println("Could not report event " + type);
		}
    }
    
    /**
     * The list of users and their IP addresses.
     * @return An array of Strings in the form: <IP>:<Port> - <Username>
     */
    public String[] getClientList() {
    	String[] result = new String[users.size()];
    	Iterator<Entry<String,ClientHandler>> iter = users.entrySet().iterator();
    	int i = 0;
    	while (iter.hasNext()) {
    		Entry<String,ClientHandler> entry = iter.next();
    		ClientHandler ch = entry.getValue();
    		result[i++] = ch.getClientIP() + ':' + ch.getClientTimestampPort() + " - " + entry.getKey();
    	}
    	return result;
    }
    
    /**
     * To be used for incoming signed bids.
     * @param user
     * @param id
     * @param amount
     * @param timestamp
     * @return The appropriate response of the server.
     */
    public String signedBidAuction(String user, int id, double amount, long timestamp) {
    	Auction a = auctions.get(id);
    	if (a == null) {
    		a = endedAuctions.get(id);
    	}
    	if (a != null) {
    		return a.delayedBid(new Bid(user, amount, timestamp));
    	} else {
    		return "There was no auction with ID: " + id + "!";
    	}
    }
}
